package qr.server.managers;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;

import qr.core.Game;
import qr.core.Leaderboard;
import qr.core.Location;
import qr.core.Player;

/**
 * Convenience class for preserving and mangling persistable objects.
 * 
 * @author Brian
 *
 */
public class TransactionGateway {
	
	public static final String JDBC_CONNECTION_PRODUCTION = "production";
	public static final String JDBC_CONNECTION_TESTING = "testing";
	
	private static TransactionGateway INSTANCE;
	
	private EntityManagerFactory factory;
	private EntityManager manager;
	
	static {
		System.setProperty("objectdb.home", "db");
	}
	
	/**
	 * Constructs a transaction gateway with the specified connection.
	 * 
	 * Persistence units are defined under META-INF/persistence.xml
	 * 
	 * @param connection
	 */
	protected TransactionGateway(String persistenceUnit) {
		this.factory = Persistence.createEntityManagerFactory(persistenceUnit);
		this.manager = this.factory.createEntityManager();
	}
	
	/**
	 * Get production instance of gateway.
	 * 
	 * @return transaction gateway
	 */
	public static TransactionGateway getInstance() {
		if (TransactionGateway.INSTANCE == null)
			TransactionGateway.INSTANCE = new TransactionGateway(JDBC_CONNECTION_PRODUCTION);
		
		return TransactionGateway.INSTANCE;
	}
	
	/**
	 * Close connections to database.
	 */
	public void close() {
		this.manager.close();
		this.factory.close();
	}
	
	/**
	 * Save an object.
	 * 
	 * @param unsaved object
	 * @return saved object
	 */
	public <T> T persist(T object) {
		EntityTransaction transaction = manager.getTransaction();
		
		transaction.begin();
		
		manager.persist(object);
		
		transaction.commit();
		
		manager.refresh(object);
		
		return object;
	}
	
	public <T> T refresh(T object) {
		manager.refresh(object);
		
		return object;
	}
	
	protected EntityManager getEntityManager() {
		return this.manager;
	}
	
	public Game getCurrentGame() {
		TypedQuery<Game> query = manager.createQuery("SELECT g FROM Game g WHERE g.isStarted", Game.class);
		
		int count = query.getMaxResults();
		
		if (count == 0) {
			throw new RuntimeException("No games have been started. There is no active game.");
		}
		
		if (count > 1) {
			throw new RuntimeException("There are more than one started/active game.");
		}
		
		Game game = query.getSingleResult();
		
		return game;
	}
	
	private void stopAllGames() {
		List<Game> games = manager
				.createQuery("SELECT g FROM Game g")
				.getResultList();
		
		manager.getTransaction().begin();
		
		for (Game g : games)
			g.setIsStarted(false);
		
		manager.getTransaction().commit();
	}
	
	public void startGame(Game game) {
		assert manager.contains(game);
		
		stopAllGames();
		
		manager.getTransaction().begin();
		
		game.setIsStarted(true);
		
		manager.getTransaction().commit();
	}
	
	public boolean hasStartedGame() {
		return manager.createQuery("SELECT COUNT(g) FROM Game g WHERE g.isStarted", Long.class).getSingleResult() == 1;
	}
	
	public boolean hasPlayerInCurrentGame(Player player) {
		Game game = this.getCurrentGame();
		for (Player p : game.getPlayers()) {
			if (p.getName().equals(player.getName())) {
				return true;
			}
		}
		
		return false;
	}
	
	public Player registerPlayer(Player player) {
		EntityTransaction trans = manager.getTransaction();

		trans.begin();
		
		Game game = getCurrentGame();
		
		manager.persist(player);
		
		game.getLeaderboard().addPlayer(player);
		
		for (Player p : game.getLeaderboard().getPlayers()) {
			System.out.println(p.getName());
		}
		
		manager.refresh(player);
		
		trans.commit();
		
		return player;
	}
	
	public boolean hasPlayer(String name) {
		return this.manager
			.createQuery("SELECT COUNT(p) FROM Player p WHERE p.name = :name", Long.class)
			.setParameter("name", name)
			.getSingleResult() != 0;
	}
	
	public void addPlayerToLeaderboard(Player player, Leaderboard leaderboard) {
		EntityTransaction transaction = manager.getTransaction();
		
		transaction.begin();
		
		manager.refresh(player);
		manager.refresh(leaderboard);
		
		leaderboard.addPlayer(player);
		
		transaction.commit();
	}
	
	public Leaderboard getLeaderboard() {
		EntityTransaction transaction = manager.getTransaction();
		transaction.begin();
		
		Game game = getCurrentGame();
		
		List<Player> players = game.getLeaderboard().getPlayers();

		Collections.sort(players, new Comparator<Player>() {
			@Override
			public int compare(Player o1, Player o2) {
				return o2.getFoundLocation().size() - o1.getFoundLocation().size();
			}
		});
		
		transaction.commit();
		
		return game.getLeaderboard();
	}
	
	public Player getPlayerWithUsername(String name) {
		return manager.createQuery("SELECT p FROM Player p WHERE p.name == :name", Player.class)
			.setParameter("name", name)
			.getSingleResult();
	}
	
	/**
	 * Retrieves a list of all known locations.
	 * 
	 * @return all locations
	 */
	public List<Location> getLocations() {
		TypedQuery<Location> query =
				manager.createQuery("SELECT location FROM Location location", Location.class);
		
		List<Location> result = query.getResultList();
		
		return result;
	}
	
	/**
	 * Retrieves a single location by id
	 * 
	 * @param id
	 * @return location
	 */
	public Location getLocation(long id) {
		TypedQuery<Location> query =
				manager.createQuery("SELECT location FROM Location location where location.id = :id", Location.class);
		
		query.setParameter("id", id);
		
		if (query.getMaxResults() == 0)
			throw new RuntimeException("No locations found with id " + id);
		
		return query.getSingleResult();
	}
	
	public Player checkInPlayer(Player player, Location location) {
		EntityTransaction transaction = manager.getTransaction();
		
		transaction.begin();
		
		player.foundLocation(location);
		
		transaction.commit();
		
		return player;
	}
	
	public List<Game> getAllGames() {
		return manager
			.createQuery("SELECT g FROM Game g", Game.class)
			.getResultList();
	}

	public boolean isManaged(Object o) {
		return manager.contains(o);
	}
	
	public void startTransaction() {
		manager.flush();
		manager.getTransaction().begin();
	}
	
	public boolean endTransaction() {
		try {
			manager.getTransaction().commit();
			manager.flush();
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
		
		return true;
	}
	
	public void deleteGame(long id) {
		EntityTransaction transaction = manager.getTransaction();
		transaction.begin();
		
		Game target = manager.find(Game.class, id);
		manager.remove(target);
		
		transaction.commit();
	}
	
	public void deleteLocation(long id) {
		EntityTransaction transaction = manager.getTransaction();
		transaction.begin();
		
		Location target = manager.find(Location.class, id);
		manager.remove(target);
		
		transaction.commit();
	}
}
